#!/usr/bin/env ruby
require 'rubygems'
require 'rubygems/dependency_installer'
require 'rubygems/uninstaller'
require 'thor'
require 'fileutils'
require 'yaml'

module MerbThorHelper
  
  private
    
  # The current working directory, or Merb app root (--merb-root option).
  def working_dir
    @_working_dir ||= File.expand_path(options['merb-root'] || Dir.pwd)
  end
  
  # We should have a ./src dir for local and system-wide management.
  def source_dir
    @_source_dir  ||= File.join(working_dir, 'src')
    create_if_missing(@_source_dir)
    @_source_dir
  end
  
  # If a local ./gems dir is found, it means we are in a Merb app.
  def application?
    gem_dir
  end
  
  # If a local ./gems dir is found, return it.
  def gem_dir
    if File.directory?(dir = File.join(working_dir, 'gems'))
      dir
    end
  end
  
  # If we're in a Merb app, we can have a ./bin directory;
  # create it if it's not there.
  def bin_dir
    @_bin_dir ||= begin
      if gem_dir
        dir = File.join(working_dir, 'bin') 
        create_if_missing(dir)
        dir
      end
    end
  end
  
  # Helper to create dir unless it exists.
  def create_if_missing(path)
    FileUtils.mkdir(path) unless File.exists?(path)
  end
  
  # Create a modified executable wrapper in the app's ./bin directory.
  def ensure_local_bin_for(*gems)
    if bin_dir && File.directory?(bin_dir)
      gems.each do |gem|
        if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{gem}-*.gemspec")].last
          spec = Gem::Specification.load(gemspec_path)
          spec.executables.each do |exec|             
            if File.exists?(executable = File.join(gem_dir, 'bin', exec))
              local_executable = File.join(bin_dir, exec)
              puts "Adding local executable #{local_executable}"
              File.open(local_executable, 'w', 0755) do |f| 
                f.write(executable_wrapper(spec, exec))
              end
            end
          end
        end
      end
    end
  end
  
  def executable_wrapper(spec, bin_file_name)
    <<-TEXT
#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}
#
# This file was generated by merb.thor.
#
# The application '#{spec.name}' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

if File.directory?(gems_dir = File.join(File.dirname(__FILE__), '..', 'gems'))
  $BUNDLE = true; Gem.clear_paths; Gem.path.unshift(gems_dir)
end

version = "#{Gem::Requirement.default}"

if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
  version = $1
  ARGV.shift
end

gem '#{spec.name}', version
load '#{bin_file_name}'
TEXT
  end
  
end

# TODO
# - a task to figure out an app's dependencies
# - pulling a specific UUID/Tag (gitspec hash) with clone/update
# - a 'deploy' task (in addition to 'redeploy' ?)
# - eventually take a --orm option for the 'merb-stack' type of tasks

class Merb < Thor
  
  class SourcePathMissing < Exception
  end
  
  class GemPathMissing < Exception
  end
  
  class GemInstallError < Exception
  end
  
  class GemUninstallError < Exception
  end
  
  # Install a Merb stack from stable RubyForge gems. Optionally install a 
  # suitable Rack adapter/server when setting --adapter to one of the 
  # following: mongrel, emongrel, thin or ebb.
  
  desc 'stable', 'Install extlib, merb-core and merb-more from rubygems'
  method_options "--merb-root" => :optional,
                 "--adapter"   => :optional
  def stable
    adapters = %w[mongrel emongrel thin ebb]
    stable = Stable.new
    stable.options = options
    if stable.core && stable.more
      puts "Installed extlib, merb-core and merb-more"
      if options[:adapter] && adapters.include?(options[:adapter]) &&
        stable.refresh_from_gems(options[:adapter])
        puts "Installed #{options[:adapter]}"
      elsif options[:adapter]
        puts "Please specify one of the following adapters: #{adapters.join(' ')}"
      end
    end  
  end  

  class Stable < Thor

    # The Stable tasks deal with known -stable- gems; available
    # as shortcuts to Merb and DataMapper gems.
    #
    # These are pulled from rubyforge and installed into into the 
    # desired gems dir (either system-wide or into the application's 
    # gems directory).
    
    include MerbThorHelper
    
    # Gets latest gem versions from RubyForge and installs them.
    #
    # Examples:
    #
    # thor merb:edge:core
    # thor merb:edge:core --merb-root ./path/to/your/app
    # thor merb:edge:core --sources ./path/to/sources.yml
    
    desc 'core', 'Install extlib and merb-core from git HEAD'
    method_options "--merb-root" => :optional
    def core
      refresh_from_gems 'extlib', 'merb-core'
      ensure_local_bin_for('merb-core', 'rake', 'rspec', 'thor')
    end
    
    desc 'more', 'Install merb-more from rubygems'
    method_options "--merb-root" => :optional
    def more
      refresh_from_gems 'merb-more'
      ensure_local_bin_for('merb-gen')
    end
    
    desc 'plugins', 'Install merb-plugins from rubygems'
    method_options "--merb-root" => :optional
    def plugins
      refresh_from_gems 'merb-plugins'
    end
    
    desc 'dm_core', 'Install dm-core from rubygems'
    method_options "--merb-root" => :optional
    def dm_core
      refresh_from_gems 'extlib', 'dm-core'
    end
    
    desc 'dm_more', 'Install dm-more from rubygems'
    method_options "--merb-root" => :optional
    def dm_more
      refresh_from_gems 'extlib', 'dm-core', 'dm-more'
    end
    
    # Pull from RubyForge and install.
    def refresh_from_gems(*components)
      gems = Gems.new
      gems.options = options
      components.all? { |name| gems.install(name) }
    end
    
  end

  # Retrieve latest Merb versions from git and optionally install them.
  #
  # Note: the --sources option takes a path to a YAML file
  # with a regular Hash mapping gem names to git urls.
  #
  # Examples:
  #
  # thor merb:edge
  # thor merb:edge --install
  # thor merb:edge --merb-root ./path/to/your/app
  # thor merb:edge --sources ./path/to/sources.yml

  desc 'edge', 'Install extlib, merb-core and merb-more from git HEAD'
  method_options "--merb-root" => :optional, 
                 "--sources"   => :optional,
                 "--install"   => :boolean
  def edge
    edge = Edge.new
    edge.options = options
    edge.core
    edge.more
  end

  class Edge < Thor
    
    # The Edge tasks deal with known gems from the bleeding edge; available
    # as shortcuts to Merb and DataMapper gems.
    #
    # These are pulled from git and optionally installed into into the 
    # desired gems dir (either system-wide or into the application's 
    # gems directory).
    
    include MerbThorHelper
    
    # Gets latest gem versions from git - optionally installs them.
    #
    # Note: the --sources option takes a path to a YAML file
    # with a regular Hash mapping gem names to git urls,
    # allowing pulling forks of the official repositories.
    #
    # Examples:
    #
    # thor merb:edge:core
    # thor merb:edge:core --install
    # thor merb:edge:core --merb-root ./path/to/your/app
    # thor merb:edge:core --sources ./path/to/sources.yml
    
    desc 'core', 'Update extlib and merb-core from git HEAD'
    method_options "--merb-root" => :optional, 
                   "--sources"   => :optional,
                   "--install"   => :boolean
    def core
      refresh_from_source 'thor', 'extlib', 'merb-core'
      ensure_local_bin_for('merb-core', 'rake', 'rspec', 'thor')
    end
    
    desc 'more', 'Update merb-more from git HEAD'
    method_options "--merb-root" => :optional, 
                   "--sources"   => :optional,
                   "--install"   => :boolean
    def more
      refresh_from_source 'merb-more'
      ensure_local_bin_for('merb-gen')
    end
    
    desc 'plugins', 'Update merb-plugins from git HEAD'
    method_options "--merb-root" => :optional, 
                   "--sources"   => :optional,
                   "--install"   => :boolean
    def plugins
      refresh_from_source 'merb-plugins'
    end
    
    desc 'dm_core', 'Update dm-core from git HEAD'
    method_options "--merb-root" => :optional, 
                   "--sources"   => :optional,
                   "--install"   => :boolean
    def dm_core
      refresh_from_source 'extlib', 'dm-core'
    end
    
    desc 'dm_more', 'Update dm-more from git HEAD'
    method_options "--merb-root" => :optional, 
                   "--sources"   => :optional,
                   "--install"   => :boolean
    def dm_more
      refresh_from_source 'extlib', 'dm-core', 'dm-more'
    end
    
    private
    
    # Pull from git and optionally install the resulting gems.
    def refresh_from_source(*components)
      source = Source.new
      source.options = options
      components.each do |name|
        source.clone(name)
        source.install(name) if options[:install]
      end
    end
    
  end
    
  class Source < Thor
    
    # The Source tasks deal with gem source packages - mainly from github.
    # Any directory inside ./src is regarded as a gem that can be packaged
    # and installed from there into the desired gems dir (either system-wide
    # or into the application's gems directory).
    
    include MerbThorHelper
    
    # Install a particular gem from source. 
    #
    # If a local ./gems dir is found, or --merb-root is given
    # the gems will be installed locally into that directory.
    #
    # Note that this task doesn't retrieve any (new) source from git;
    # To update and install you'd execute the following two tasks:
    #
    # thor merb:source:update merb-core
    # thor merb:source:install merb-core
    #
    # Alternatively, look at merb:edge and merb:edge:* with --install.
    #
    # Examples:
    # 
    # thor merb:source:install merb-core
    # thor merb:source:install merb-more
    # thor merb:source:install merb-more/merb-slices
    # thor merb:source:install merb-plugins/merb_helpers
    # thor merb:source:install merb-core --merb-root ./path/to/your/app
    
    desc 'install GEM_NAME', 'Install a rubygem from (git) source'
    method_options "--merb-root" => :optional
    def install(name)
      puts "Installing #{name}..."
      gem_src_dir = File.join(source_dir, name)
      opts = {}
      opts[:install_dir] = gem_dir   if gem_dir
      Merb.install_gem_from_src(gem_src_dir, opts)
    rescue Merb::SourcePathMissing
      puts "Missing rubygem source path: #{gem_src_dir}"
    rescue Merb::GemPathMissing
      puts "Missing rubygems path: #{gem_dir}"
    rescue => e
      puts "Failed to install #{name} (#{e.message})"
    end
    
    # Clone a git repository into ./src. The repository can be
    # a direct git url or a known -named- repository.
    # 
    # Examples:
    #
    # thor merb:source:clone dm-core
    # thor merb:source:clone dm-core --sources ./path/to/sources.yml
    # thor merb:source:clone git://github.com/sam/dm-core.git
    
    desc 'clone REPOSITORY', 'Clone a git repository into ./src'
    method_options "--sources" => :optional
    def clone(repository)
      if repository =~ /^git:\/\//
        repository_url = repository
      elsif url = Merb.repos(options[:sources])[repository]
        repository_url = url
      end
      
      if repository_url
        repository_name = repository_url[/([\w+|-]+)\.git/u, 1]
        fork_name = repository_url[/.com\/+?(.+)\/.+\.git/u, 1]
        local_repo_path =  "#{source_dir}/#{repository_name}"
      
        if File.directory?(local_repo_path)
          puts "\n#{repository_name} repository exists, updating or branching instead of cloning..."
          FileUtils.cd(local_repo_path) do

            # to avoid conflicts we need to set a remote branch for non official repos
            existing_repos  = `git remote -v`.split("\n").map{|branch| branch.split(/\s+/)}
            origin_repo_url = existing_repos.detect{ |r| r.first == "origin" }.last
        
            # pull from the original repository - no branching needed
            if repository_url == origin_repo_url
              puts "Pulling from #{repository_url}"            
              system %{
                git fetch
                git checkout master
                git rebase origin/master
              }
            # update and switch to a branch for a particular github fork
            elsif existing_repos.map{ |r| r.last }.include?(repository_url)
              puts "Switching to remote branch: #{fork_name}"
              `git checkout -b #{fork_name} #{fork_name}/master`
              `git rebase #{fork_name}/master`            
            # create a new remote branch for a particular github fork 
            else
              puts "Add a new remote branch: #{fork_name}"
              `git remote add -f #{fork_name} #{repository_url}`
              `git checkout -b#{fork_name} #{fork_name}/master`
            end
          end
        else
          FileUtils.cd(source_dir) do
            puts "\nCloning #{repository_name} repository from #{repository_url}..."
            system("git clone --depth=1 #{repository_url} ")
          end
        end
      else
        puts "No valid repository url given"
      end
    end
    
    # Update a specific gem source directory from git. See #clone.
    
    desc 'update REPOSITORY_URL', 'Update a git repository in ./src'
    alias :update :clone
    
    # Update all gem sources from git - based on the current branch.
    
    desc 'refresh', 'Pull fresh copies of all source gems'
    def refresh
      repos = Dir["#{source_dir}/*"]
      repos.each do |repo|
        next unless File.directory?(repo) && File.exists?(File.join(repo, '.git'))
        FileUtils.cd(repo) do
          puts "Refreshing #{File.basename(repo)}"
          branch = `git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1) /'`[/\* (.+)/, 1]
          system %{git rebase #{branch}}
        end
      end
    end

  end
  
  class Gems < Thor
    
    # The Gems tasks deal directly with rubygems, either through remotely
    # available sources (rubyforge for example) or by searching the
    # system-wide gem cache for matching gems. The gems are installed from 
    # there into the desired gems dir (either system-wide or into the 
    # application's gems directory).
    
    include MerbThorHelper
    
    # Install a gem and its dependencies.
    #
    # If a local ./gems dir is found, or --merb-root is given
    # the gems will be installed locally into that directory.
    #
    # The option --cache will look in the system's gem cache
    # for the latest version and install it in the apps' gems.
    # This is particularly handy for gems that aren't available
    # through rubyforge.org - like in-house merb slices etc.
    #
    # Examples:
    #
    # thor merb:gems:install merb-core
    # thor merb:gems:install merb-core --cache
    # thor merb:gems:install merb-core --version 0.9.7
    # thor merb:gems:install merb-core --merb-root ./path/to/your/app
    
    desc 'install GEM_NAME', 'Install a gem from rubygems'
    method_options "--version"   => :optional, 
                   "--merb-root" => :optional,
                   "--cache"     => :boolean
    def install(name)
      puts "Installing #{name}..."
      opts = {}
      opts[:version] = options[:version]
      opts[:cache] = options[:cache] if gem_dir
      opts[:install_dir] = gem_dir   if gem_dir
      Merb.install_gem(name, opts)
    rescue => e
      puts "Failed to install #{name} (#{e.message})"
    end
    
    # Update a gem and its dependencies.
    #
    # If a local ./gems dir is found, or --merb-root is given
    # the gems will be installed locally into that directory.
    #
    # The option --cache will look in the system's gem cache
    # for the latest version and install it in the apps' gems.
    # This is particularly handy for gems that aren't available
    # through rubyforge.org - like in-house merb slices etc.
    #
    # Examples:
    #
    # thor merb:gems:update merb-core
    # thor merb:gems:update merb-core --cache
    # thor merb:gems:update merb-core --merb-root ./path/to/your/app
    
    desc 'update GEM_NAME', 'Update a gem from rubygems'
    method_options "--merb-root" => :optional,
                   "--cache"     => :boolean
    def update(name)
      puts "Updating #{name}..."
      opts = {}
      if gem_dir
        if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{name}-*.gemspec")].last
          gemspec = Gem::Specification.load(gemspec_path)
          opts[:version] = Gem::Requirement.new [">#{gemspec.version}"]
        end
        opts[:install_dir] = gem_dir
        opts[:cache] = options[:cache]
      end
      Merb.install_gem(name, opts)
    rescue => e
      puts "Failed to update #{name} (#{e.message})"
    end
    
    # Uninstall a gem - ignores dependencies.
    #
    # If a local ./gems dir is found, or --merb-root is given
    # the gems will be uninstalled locally from that directory.
    #
    # Examples:
    #
    # thor merb:gems:uninstall merb-core
    # thor merb:gems:uninstall merb-core --all
    # thor merb:gems:uninstall merb-core --version 0.9.7
    # thor merb:gems:uninstall merb-core --merb-root ./path/to/your/app
    
    desc 'install GEM_NAME', 'Install a gem from rubygems'
    desc 'uninstall GEM_NAME', 'Uninstall a gem'
    method_options "--version"   => :optional, 
                   "--merb-root" => :optional,
                   "--all" => :boolean
    def uninstall(name)
      puts "Uninstalling #{name}..."
      opts = {}
      opts[:ignore] = true
      opts[:all] = options[:all]
      opts[:executables] = true
      opts[:version] = options[:version]
      opts[:install_dir] = gem_dir if gem_dir
      Merb.uninstall_gem(name, opts)
    rescue => e
      puts "Failed to uninstall #{name} (#{e.message})"  
    end
    
    # Completely remove a gem and all its versions - ignores dependencies.
    #
    # If a local ./gems dir is found, or --merb-root is given
    # the gems will be uninstalled locally from that directory.
    #
    # Examples:
    #
    # thor merb:gems:wipe merb-core
    # thor merb:gems:wipe merb-core --merb-root ./path/to/your/app
    
    desc 'wipe GEM_NAME', 'Remove a gem completely'
    method_options "--merb-root" => :optional
    def wipe(name)
      puts "Wiping #{name}..."
      opts = {}
      opts[:ignore] = true
      opts[:all] = true
      opts[:executables] = true
      opts[:install_dir] = gem_dir if gem_dir
      Merb.uninstall_gem(name, opts)
    rescue => e
      puts "Failed to wipe #{name} (#{e.message})"  
    end
    
    # This task should be executed as part of a deployment setup, where
    # the deployment system runs this after the app has been installed.
    # Usually triggered by Capistrano, God...
    #
    # It will regenerate gems from the bundled gems cache for any gem
    # that has C extensions - which need to be recompiled for the target
    # deployment platform.
    
    desc 'redeploy', 'Recreate any binary gems on the target deployment platform'
    def redeploy
      require 'tempfile' # for 
      if File.directory?(specs_dir = File.join(gem_dir, 'specifications')) &&
        File.directory?(cache_dir = File.join(gem_dir, 'cache'))
        Dir[File.join(specs_dir, '*.gemspec')].each do |gemspec_path|
          unless (gemspec = Gem::Specification.load(gemspec_path)).extensions.empty?
            if File.exists?(gem_file = File.join(cache_dir, "#{gemspec.full_name}.gem"))
              gem_file_copy = File.join(Dir::tmpdir, File.basename(gem_file))
              # Copy the gem to a temporary file, because otherwise RubyGems/FileUtils
              # will complain about copying identical files (same source/destination).
              FileUtils.cp(gem_file, gem_file_copy) 
              Merb.install_gem(gem_file_copy, :install_dir => gem_dir)
              File.delete(gem_file_copy)
            end
          end
        end
      else
        puts "No application local gems directory found"
      end
    end
    
  end
  
  class << self
        
    # Default Git repositories - pass source_config option
    # to load a yaml configuration file.
    def repos(source_config = nil)
      @_repos ||= begin
        repositories = {
          'merb-core'     => "git://github.com/wycats/merb-core.git",
          'merb-more'     => "git://github.com/wycats/merb-more.git",
          'merb-plugins'  => "git://github.com/wycats/merb-plugins.git",
          'extlib'        => "git://github.com/sam/extlib.git",
          'dm-core'       => "git://github.com/sam/dm-core.git",
          'dm-more'       => "git://github.com/sam/dm-more.git",
          'thor'          => "git://github.com/wycats/thor.git"
          }
      end
      if source_config && File.exists?(source_config)
        @_repos.merge(YAML.load(File.read(source_config)))
      else
        @_repos
      end
    end
    
    # Install a gem - looks remotely and local gem cache;
    # won't process rdoc or ri options.
    def install_gem(gem, options = {})
      from_cache = (options.key?(:cache) && options.delete(:cache))
      if from_cache
        install_gem_from_cache(gem, options)
      else
        version = options.delete(:version)
        Gem.configuration.update_sources = false
        installer = Gem::DependencyInstaller.new(options.merge(:user_install => false))
        exception = nil
        begin
          installer.install gem, version
        rescue Gem::InstallError => e
          exception = e
        rescue Gem::GemNotFoundException => e
          if from_cache && gem_file = find_gem_in_cache(gem, version)
            puts "Located #{gem} in gem cache..."
            installer.install gem_file
          else
            exception = e
          end
        rescue => e
          exception = e
        end
        if installer.installed_gems.empty? && exception
          puts "Failed to install gem '#{gem}' (#{exception.message})"
        end
        installer.installed_gems.each do |spec|
          puts "Successfully installed #{spec.full_name}"
        end
      end
    end
    
    # Install a gem - looks in the system's gem cache instead of remotely;
    # won't process rdoc or ri options.
    def install_gem_from_cache(gem, options = {})
      version = options.delete(:version)
      Gem.configuration.update_sources = false
      installer = Gem::DependencyInstaller.new(options.merge(:user_install => false))
      exception = nil
      begin
        if gem_file = find_gem_in_cache(gem, version)
          puts "Located #{gem} in gem cache..."
          installer.install gem_file
        else
          raise Gem::InstallError, "Unknown gem #{gem}" 
        end
      rescue Gem::InstallError => e
        exception = e
      end
      if installer.installed_gems.empty? && exception
        puts "Failed to install gem '#{gem}' (#{e.message})"
      end
      installer.installed_gems.each do |spec|
        puts "Successfully installed #{spec.full_name}"
      end
    end
  
    # Install a gem from source - builds and packages it first then installs it.
    def install_gem_from_src(gem_src_dir, options = {})
      raise SourcePathMissing unless File.directory?(gem_src_dir)
      raise GemPathMissing if options[:install_dir] && !File.directory?(options[:install_dir])
  
      gem_name = File.basename(gem_src_dir)
      gem_pkg_dir = File.expand_path(File.join(gem_src_dir, 'pkg'))

      # We need to use local bin executables if available.
      thor = which('thor')
      rake = which('rake')

      # Handle pure Thor installation instead of Rake
      if File.exists?(File.join(gem_src_dir, 'Thorfile'))
        # Remove any existing packages.
        FileUtils.rm_rf(gem_pkg_dir) if File.directory?(gem_pkg_dir)
        # Create the package.
        FileUtils.cd(gem_src_dir) { system("#{thor} :package") }
        # Install the package using rubygems.
        if package = Dir[File.join(gem_pkg_dir, "#{gem_name}-*.gem")].last
          FileUtils.cd(File.dirname(package)) do
            install_gem(File.basename(package), options.dup)
            return 
          end
        else
          raise Merb::GemInstallError, "No package found for #{gem_name}"
        end
      # Handle standard installation through Rake
      else
        # Clean and regenerate any subgems for meta gems.
        Dir[File.join(gem_src_dir, '*', 'Rakefile')].each do |rakefile|
          FileUtils.cd(File.dirname(rakefile)) { system("#{rake} clobber_package; #{rake} package") }             
        end
  
        # Handle the main gem install.
        if File.exists?(File.join(gem_src_dir, 'Rakefile'))
          # Remove any existing packages.
          FileUtils.cd(gem_src_dir) { system("#{rake} clobber_package") }
          # Create the main gem pkg dir if it doesn't exist.
          FileUtils.mkdir_p(gem_pkg_dir) unless File.directory?(gem_pkg_dir)
          # Copy any subgems to the main gem pkg dir.
          Dir[File.join(gem_src_dir, '**', 'pkg', '*.gem')].each do |subgem_pkg|
            FileUtils.cp(subgem_pkg, gem_pkg_dir)
          end
    
          # Finally generate the main package and install it; subgems 
          # (dependencies) are local to the main package.
          FileUtils.cd(gem_src_dir) do 
            system("#{rake} package")
            if package = Dir[File.join(gem_pkg_dir, "#{gem_name}-*.gem")].last
              FileUtils.cd(File.dirname(package)) do
                install_gem(File.basename(package), options.dup)
                return 
              end
            else
              raise Merb::GemInstallError, "No package found for #{gem_name}"
            end
          end
        end
      end
      raise Merb::GemInstallError, "No Rakefile found for #{gem_name}"
    end
    
    # Uninstall a gem.
    def uninstall_gem(gem, options = {})
      if options[:version] && !options[:version].is_a?(Gem::Requirement)
        options[:version] = Gem::Requirement.new ["= #{version}"]
      end
      begin
        Gem::Uninstaller.new(gem, options).uninstall
      rescue => e
        raise GemUninstallError, "Failed to uninstall #{gem}"
      end
    end
    
    # Will prepend sudo on a suitable platform.
    def sudo
      @_sudo ||= begin 
        windows = PLATFORM =~ /win32|cygwin/ rescue nil
        windows ? "" : "sudo "
      end
    end
    
    # Use the local bin/* executables if available.
    def which(executable)
      if File.executable?(exec = File.join(Dir.pwd, 'bin', executable))
        exec
      else
        executable
      end
    end
    
    private
    
    def find_gem_in_cache(gem, version)
      spec = if version
        version = Gem::Requirement.new ["= #{version}"] unless version.is_a?(Gem::Requirement)
        Gem.source_index.find_name(gem, version).first
      else
        Gem.source_index.find_name(gem).sort_by { |g| g.version }.last
      end
      if spec && File.exists?(gem_file = "#{spec.installation_path}/cache/#{spec.full_name}.gem")
        gem_file
      end
    end
    
  end
  
  class Tasks < Thor
    
    include MerbThorHelper
    
    # Install Thor, Rake and RSpec into the local gems dir, by copying it from 
    # the system-wide rubygems cache - which is OK since we needed it to run 
    # this task already.
    #
    # After this we don't need the system-wide rubygems anymore, as all required
    # executables are available in the local ./bin directory.
    #
    # RSpec is needed here because source installs might fail when running
    # rake tasks where spec/rake/spectask has been required.
    
    desc 'setup', 'Install Thor, Rake and RSpec in the local gems dir'
    method_options "--merb-root" => :optional
    def setup
      if $0 =~ /^(\.\/)?bin\/thor$/
        puts "You cannot run the setup from #{$0} - try #{File.basename($0)} merb:tasks:setup instead"
        return
      end
      create_if_missing(File.join(working_dir, 'gems'))
      Merb.install_gem('thor',  :cache => true, :install_dir => gem_dir)
      Merb.install_gem('rake',  :cache => true, :install_dir => gem_dir)
      Merb.install_gem('rspec', :cache => true, :install_dir => gem_dir)
      ensure_local_bin_for('thor', 'rake', 'rspec')
    end

  end
  
end
